Skip to content

Give the Win32 render window a File/View menu and hotkeys - #8

Open
dougchansan wants to merge 3 commits into
ExpansionPak:moderngekko-vendorfrom
dougchansan:platform-win32-state-menu
Open

Give the Win32 render window a File/View menu and hotkeys#8
dougchansan wants to merge 3 commits into
ExpansionPak:moderngekko-vendorfrom
dougchansan:platform-win32-state-menu

Conversation

@dougchansan

@dougchansan dougchansan commented Aug 5, 2026

Copy link
Copy Markdown

The DolphinNoGUI window has no way to save or load a state, and Esc as its only key. Getting back to a scene means booting and playing in, every time.

File menu open over a running game

File carries Save State (F1), a Load State submenu, and Pause. View carries Fullscreen (F11 or Alt+Enter) and Mute Audio. Space held is fast-forward.

Load State submenu

No new frontend obligations

All of it goes through Core, State and Config directly rather than new Host_* hooks. Adding hooks would oblige every frontend to implement them before it could build; this way nothing outside this file has to change.

Details worth flagging

States are written to and listed from Dolphin's own StateSaves directory, named by wall clock (state-YYYYmmdd-HHMMSS.sav) rather than by slot, so repeated saves accumulate instead of overwriting one another and sort meaningfully by name.

The Load State list is rebuilt whenever the menu opens, because states appear while it is closed. It is newest-first with a filename tiebreak, so the order does not shuffle between opens on a filesystem whose timestamps are coarse enough for two states to share a write time. The range of command ids is bounded and the list truncated to it, rather than running past the end.

F1 ignores auto-repeat — bit 30 of lParam is the previous key state — or holding it down would write a state every few milliseconds.

Fast-forward is cancelled on WM_KILLFOCUS. Without that, alt-tabbing mid-hold delivers the key-up to another window and leaves emulation stuck at 2x.

Pause and Mute read their checked state from the core when the menu opens, so the tick cannot drift out of step with an emulation that was paused or muted some other way.

Fullscreen hides the menu bar and restores the previous window style and rect on the way back, rather than assuming a default.

Testing

Built with MSVC/Ninja Release and exercised against a running game (Pokémon Colosseum, booted into a savestate), driving each command by posting it to the window and then checking it had an effect:

Result
F1 writes a state new state-20260804-231457.sav appeared
Save State menu item second state written
Pause halts emulation, and resumes CPU load 1.031 → 0.018 → 1.042 cpu-seconds per wall second
Fullscreen expands and restores window width 1556 → 5120 → 1556 px
Mute check mark tracks the config absent before the toggle, present after
Load State lists newest-first screenshot above

One measurement note, since it nearly produced a wrong answer: the window title is not a usable pause signal. When the core pauses, the title stops being updated and keeps whatever FPS it last showed, so a paused run reads identical to a running one — my first attempt "failed" Pause on that basis. Process CPU time cannot be faked that way.

How this fits

These land together as a set: savestates working end to end, the Windows build and test suite being usable at all, and CI so none of it regresses unnoticed. This PR is RC #8.

MG #18 Bump vendored RecompCore prerequisite - without it the launcher cannot compile on Windows (C sources get a C++ PCH)
RC #7 Interpreter.cpp include prerequisite - core does not compile without it
RC #8 In-game File/View menu and hotkeys introduces Core/SavestateLayout.h, the one definition of where savestates live, what they are called and how they are ordered
MG #21 Launcher savestate picker, --load-state consumes that header, so the launcher and the in-game menu cannot disagree
MG #22 Make the test suite pass on Windows land before CI, or the first run is red on day one
MG #23 Build and test on PRs, three platforms the reason the rest stayed broken unnoticed
RC #10 CI on PRs, the vendored branch, and Windows same gap, other repo; its Windows job fails until RC #7 lands
MG #19 --opt-level, default -O2 independent
MG #20 Cache-domain affinity independent

Suggested order: #18 and RC #7, then RC #8, then MG #21; MG #22 before MG #23. The rest are independent.

Verified on three platforms: Windows (MSVC + clang), Linux (g++ 15.2, x86_64) and macOS 26.1 (clang, arm64). The portable pieces - the savestate layout and its tests, frontend_config, dol_patch - build and pass on all three. Windows-only pieces are guarded and their tests registered behind if(WIN32).

unknown_instruction reads g_static_recomp_core and ppc_state, but the
translation unit includes neither the header that declares the former nor
a binding for the latter, so core does not compile:

  Interpreter.cpp(294): error C2065: 'g_static_recomp_core': undeclared identifier
  Interpreter.cpp(299): error C2065: 'ppc_state': undeclared identifier

It is not reached through the PCH either -- pch.h does not pull in
StaticRecompCore.h.

The ppc_state binding already existed further down the same function, past
the first use. Moving it to the top serves both uses rather than adding a
second one, which would be a redefinition.
DolphinNoGUI's window had no way to save or load a state, and Esc as the
only key. Getting back to a scene meant booting and playing in.

File carries Save State (F1), a Load State submenu, and Pause. View
carries Fullscreen (F11 or Alt+Enter) and Mute Audio. Space held is
fast-forward.

All of it goes through Core, State and Config directly rather than new
Host_* hooks, so no frontend has to implement anything to pick this up.

States are written to and listed from Dolphin's own StateSaves directory,
named by wall clock rather than by slot, so repeated saves accumulate
instead of overwriting each other and sort meaningfully by name. The Load
State list is rebuilt whenever the menu opens, because states appear
while it is closed; it is newest-first with a filename tiebreak so the
order does not shuffle between opens on a filesystem with coarse
timestamps.

F1 ignores auto-repeat -- bit 30 of lParam -- or holding it down would
write a state every few milliseconds. Fast-forward is also cancelled on
WM_KILLFOCUS, so alt-tabbing mid-hold cannot leave emulation stuck at 2x
with the key-up delivered elsewhere.
Where savestates live, what they are called, and what order they list in
are facts more than one thing has to agree about: the emulator writes and
lists them from its own menu, and a frontend offers the same set before
boot. Stated twice they drift, and the symptom is the same directory
coming back in a different order depending on where you look -- confusing
to chase, and easy to cause by editing one copy.

Core/SavestateLayout.h holds the extension, the automatic-state prefix,
the timestamped name, and the newest-first-with-filename-tiebreak order.
It depends on nothing but the standard library, so a frontend can include
it without linking any of Dolphin. The menu added in this branch now
takes all four from it rather than restating them.

The rules are tested where they are defined -- eight cases, including
that pruning never removes a player's own saves, that slot saves in the
same directory are not listed, and that equal timestamps fall back to
filename so the order cannot shuffle between reads.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant